查看原文
其他

kotlin协程:并发 & 线程安全

安安安安卓 AndroidPub 2022-07-13

概览

我们在 java 中处理并发是家常便饭,但是协程的并发你有没有想过呢,协程是否也有java一样的并发问题?

我们知道协程是轻量级的进程,而且是可以多线程调度的。那么想想这样一个情景:

我们开启1000个协程,每个协程中对count进行自增,协程执行完成后能否拿到 count==1000 的结果,答案在后面的章节中,最终结论就是 kotlin 也是需要处理并发的

那么这种并发该如何处理呢,我想先给你说的是,协程有自己的一套并发规则,你应该试图优先用 kotlin 的并发方法来处理协程的并发

本文主要讲几种协程处理并发的例子

模拟协程并发

模拟案例

既然是讲协程的并发,那么我们首先应该证明一下协程中存在并发问题,对吧,那就来吧。

先看一下模拟的代码:

  1. 代码
fun concurrent(){
    val scope = CoroutineScope(Dispatchers.Default)//创建协程作用域,Default支持并发
    var count=0
    repeat(1000){//重复1000次,每次开启一个协程,count自增1
        scope.launch {
            count++
            println(count)
        }
    }
}
fun main() = runBlocking {
    concurrent()
}

本例中,我们模拟了这样一个场景,使用自定义的作用域 scope 短时间内开启 1000 个协程,协程中每次把 count 自增 1 打印数据。如果最终打印的数据是 1000 那么说明不存在并发,如果打印的数据小于 1000,那么说明协程中也存在如 java 一样的并发问题。

  1. 日志

这里只发出最后一部分的日志:

696
695
694
693
692
691
690
703
702
  1. 结论

日志打印最终验证协程中也存在并发性的问题,那么后面的章节我们就拿出我们的协程解决方案吧。

模拟案例扩展

我们将本例中的代码做一个扩展,在concurrent方法尾部加一个1s的延时

  1. 代码

  2. 输出日志

999
1000
978
995
994
993
992
991
1000
  1. 结论

当最后一个协程执行完成时可以计算出1000这个正确结果但是打印的中间值顺序却是错乱的(这一点和java不一样),但是我们在业务代码中却没有办法获取这个正确结果产生的时间,所以我们依然要处理并发问题

解决并发的方法

解决并发的问题既可以使用 java 中的部分方式,也可以使用 kotlin 自有的方式。协程自有的处理方法适用性更广一些。

java 方式解决

如果我们想使用java的方式解决并发,那么我们可以尝试让协程运行在单线程中,也可以尝试使用Atomic方式

单线程解决方法

我们将 模拟并发章节的代码进行一些改动试试:

  1. 代码
 fun concurrent() {
    val scope = CoroutineScope(Dispatchers.Unconfined)//创建协程作用域,使用Unconfined,这样在协程被挂起前都不会改变线程,也就是说协程始终运行在单线程中
    var count = 0
    repeat(1000) {//重复1000次,每次开启一个协程,count自增1
        scope.launch {
            println("线程id:${Thread.currentThread().id}")//这个线程始终不会变,除非你在这里挂起
            count++
            println(count)
        }
    }
}
  1. 日志
994
线程id:1
995
线程id:1
996
线程id:1
997
线程id:1
998
线程id:1
999
线程id:1
1000
  1. 结论 本例代码中因为launch始终运行在单线程中所以最终输出count==1000,中间值的顺序也没有错乱

使用Atomic方式解决

上一小节中我们的代码始终运行在单线程中,所以最终输出正确结果,那么如果我们的代码不允许在单线程中有没有解决方法呢,答案当然是有, Atomic机制即可解决。

试想一下如果我们把上面的代码scope.launch 改为launch(Dispatchers.Default) ,那么线程就变成了运行在多线程中了,此时输出的最终结果一定不会是我们想要的(我已经帮你试过了,就不贴代码和日志了)

那么我们可以尝试用Atomic方式解决这个问题

  1. 代码
suspend fun concurrent() {
    var count =AtomicInteger(0)
    coroutineScope {
        repeat(10000) {//重复1000次,每次开启一个协程,count自增1
            launch(Dispatchers.Default) {
                count.incrementAndGet()
                println("计算中间值$count")
            }
        }
    }
    delay(1000)
    println("计算结果:$count")
}
  1. 日志
计算结果:10000
  1. 结论 虽然最终输出结果是正确的

但是对于负责的状态Atomic明显是处理不了的,负责的状态可以考虑后面要讲的Mutex方式

kotlin 方式解决

互斥

kotlin 为我们提供了Mutex实现线程安全,Mutex通俗点来说就是kotlin的锁,和java 的synchronized和RecentLock对应。

使用mutex.withLock {*} 即可实现数据的同步

看代码:

  1. 代码
val mutex = Mutex()

suspend fun concurrent() {
    var count =0
    coroutineScope {
        repeat(10000) {//重复1000次,每次开启一个协程,count自增1
            launch(Dispatchers.Default) {
                mutex.withLock {
                    count++
                }
                println("中间值:$count")
            }
        }
    }
    delay(1000)
    println("计算结果:$count")
}

  1. 日志
****
中间值:9993
中间值:9994
中间值:9995
中间值:9996
中间值:9997
中间值:9998
中间值:9999
中间值:10000
计算结果:10000
  1. 结论

使用Mutex方式解决并发,最终输出正确结果,并且中间值也是按顺序输出

掌握上面的几种方式基本上能处理大部分业务逻辑了

~ FIN ~



推荐阅读

AAB 什么鬼?竟敢打压鸿蒙?
【程序员必读】编程的智慧 by 王垠
建议收藏!Kotlin 线程同步的 N 种方法
再见 KAPT!使用 KSP 为 Kotlin 编译提速


加我好友拉你进技术交流群,每天干货聊不停~


↓关注公众号↓↓添加微信交流↓



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存